<?php
/**
 * URL Procedure Call
 * 
 * @version 0.7
 * @date 2010-09-07
 * @author Kain Haart <dev@mail.kain-haart.info>
 * 
 * Abstract class provides to ancestors mechanism of processing URL as method call
 * with chain-like delegating
 * 
 * UPC syntax is smth like "http://yourdomain.com/SelectingDelegate(Name)/Method([Param1,Param2])"
 */

/**
 * UPC-callable object interface
 * @author Kain
 */
interface iUPC
{
public function call($Request);
public function process($sStep,$mParams = NULL,$Request = NULL);
}

class upcException extends Exception {}

/**
 * URL Procedure Call
 */
abstract class UPC implements iUPC
{
/**
 * UPC Call method
 * Construcs UPCRequest object and execute request by calling UPCRequest::call()
 * 
 * @param mixed $Request Request to be executed
 * @return mixed Results of executing Request
 */
public final function call($Request)
	{
	# Preparing request
	if(!($Request instanceof UPCRequest))
		$Request = new UPCRequest($Request);
	return $Request->call($this);
	}
	
/**
 * Processes request step.
 *
 * @param $sStep string Action name
 * @param $mParams mixed Action parameters
 * @param UPCRequest $Request
 * @return mixed Results of processing 1 request step 
 */
public function process($sStep,$mParams = NULL,$Request = NULL)
	{
	$mResult = $this->upcMethodCall($sStep,$mParams,$Request);
	return $mResult;
	}
	
/**
 * upcHandler: Process UPC Step as Method
 * 
 * notice: class constant tip for this Handler declared in UPCPlus 
 * 
 * @param $Step string Step
 * @param $Request UPCRequest Request
 * @return mixed Result
 */
protected final function upcMethodCall($sMethod, $mParams = NULL, $Request = NULL)
	{
	# Filtering non-acceptable methods
	if($sMethod and $sMethod[0]=="_")
		throw new upcException("Trying to call shielded method via UPC");
	# Filtering UPC System methods
	if(method_exists("iUPC",$sMethod))
		throw new upcException("Trying to call UPC system method via UPC");
	# Filtering non-existing methods
	if(!method_exists($this,$sMethod))
		return NULL;
	# Filtering non-public methods
	$rAction = new ReflectionMethod($this,$sMethod);
	if(!$rAction->isPublic())
		return NULL;
	# Run Method
	$mResult = $this->$sMethod($mParams,$Request);
	# Result
	return $mResult;	
	}
}

/**
 * Extended UPC
 */
abstract class UPCPlus extends UPC implements iUPC
{	
/**
 * methodname TIP: upcHandler Method call (goes in UPC class)
 * @var string
 */
const upcHandlerMethodCall = "upcMethodCall";
	
##
# Запрос по-умолчанию
##
	
/**
 * Строка запроса по-умолчанию для метода processDefaultRequest
 * @var string
 */
protected $sDefaultRequest;
/**
 * Метод обработки запроса по-умолчанию
 * @return mixed
 */
public function processDefaultRequest()
	{
	return $this->call($this->sDefaultRequest);
	}
	
##
# Native action call mechanism
##

public function __call($sMethod,$aParams)
	{
	return $this->process($sMethod,reset($aParams));
	}
	
##
# Расширенный PROCESS c использованием Handlers & Advises 
##

/**
 * mothodname TIP: upcAdvice Alias of action
 * @var string
 */
const upcAdviceAlias = "upcAliasAdvice";

/**
 * Списки эдвайсов - имен методов для аспектно-ориентированной обработки шага запроса
 * В порядке применения
 * Пока используется только список ['before']
 * @var string[]['before']
 */	
protected $aUPCAdvicesList = array("before"=>array("upcAliasAdvice"));
	
/**
 * Список хэндлеров - имен методов для обработки шага запроса 
 * В порядке попыток использования
 * @var string[]
 */
protected $aUPCHandlersList = array(self::upcHandlerMethodCall,self::upcHandlerSortcuts);
/**
 * Указания каким хэндлером обрабатывается какое действие (кэш/форс)
 * $sActionName => $sHandlerName
 * @var string[string]
 */
protected $aUPCHandlersInstructions = array("var"=>"upcVariableHandler");
/**
 * Режим кэширования указаний применения хэндлеров
 * @var boolean
 */
protected $bUPCHandlersCache = true;

/**
 * Processes request step.
 *
 * @param string $sStep Имя шага
 * @param mixed $mParams Параметры шага
 * @param UPCRequest $Request
 * @return mixed Results of processing 1 request step 
 */
public function process($sStep,$mParams = NULL,$Request = NULL)
	{
	# Use advices
	if(isset($this->aUPCAdvicesList['before']))
		foreach($this->aUPCAdvicesList['before'] as $sUPCAdvice)
			{
			list($sStep,$mParams,$Request) = $this->$sUPCAdvice($sStep,$mParams,$Request);
			}
	# Step is blocked
	if(is_null($sStep)) return NULL;
		
	# Forced (or cached) handler is selected for this step
	if(array_key_exists($sStep,$this->aUPCHandlersInstructions))
		{
		$sUPCHandler = $this->aUPCHandlersInstructions[$sStep];
		# $UPCHandler == NULL force to not handle this step 
		if(is_null($sUPCHandler)) return NULL;
		# Handle step with according handler
		return $this->$sUPCHandler($sStep, $mParams, $Request);
		} 
		
	# Trying to handle step through list of handlers
	foreach($this->aUPCHandlersList as $sUPCHandler)
		{
		$mResult = $this->$sUPCHandler($sStep, $mParams, $Request);
		if(isset($mResult))
		break;
		}
	
	# Cache handler
	if($this->bUPCHandlersCache)
		$this->aUPCHandlersInstructions[$sStep] = isset($mResult) ? $sUPCHandler : NULL;
		
	# Result
	return $mResult;
	}
	
##
# Alias advice
##

/**
 * Список соответсвий алиасов действиям
 * $sActionAlias => $sActionName
 * @var string[string]
 */
protected $aUPCAliasInstructions = array();

/**
 * Адвайс алиасов действий
 * @param $sAction
 * @param $mParams
 * @param $Request
 * @return array [$sAction,$mParams,$Request]
 * @todo Изменять параметры за счет передачи по ссылке 
 */
protected final function upcAliasAdvice($sAction,$mParams,$Request)
	{
	if(array_key_exists($sAction,$this->aUPCAliasInstructions))
		$sAction = $this->aUPCAliasInstructions[$sAction];
	return array($sAction,$mParams,$Request);
	}
	
##
# Variable Handler
##

/**
 * Array of UPCVariable Values
 * $sVariableName=>$mVariableValue
 * @var mixed[string]
 */
protected $aUPCVariableValues = array();
/**
 * Массив имен переменных для прямого обращения
 * @var string[]
 */
protected $aUPCVariableDirectnames = array();
	
/**
 * methodname TIP: upcHandler Variables handler
 * @var string
 */
const upcHandlerVariables = "upcVariableHandler";
/**
 * upcHandler: Shortcut handler
 * NULL value can't be used
 * @param $sAction
 * @param $mParams
 * @param $Request
 * @return unknown_type
 */
protected final function upcVariableHandler($sAction, $mParams = NULL, $Request = NULL)
	{
	# Prepare params
		
	# Common variables 
	if($sAction == "var")
		{
		# NoParams - return all variables
		if(!isset($mParams) or !$mParams)
			return $this->aUPCVariableValues; 
		
		$mParams = explode(",",$mParams,2);
		$sVarName = trim($mParams[0]);
		$mValue = (sizeof($mParams)>1) ? trim($mParams[1]) : NULL;
		}
	# Direct name variables
	elseif(in_array($sAction,$this->aUPCVariableDirectnames))
		{
		$sVarName = $sAction;
		$mValue = $mParams;
		}
		
	# Do something
	
	if(!isset($mValue))	# Get variable
		{	
		$mValue = @$this->aUPCVariableValues[$sVarName];
		return $mValue;
		}
	else # Set Variable
		{
		$this->aUPCVariableValues[$sVarName] = $mValue;
		return $this;
		}
		
	}
	
##
# ClassLoader Handler
##

protected $aUPCClassloadList = array();
//TODO: Autoload flag
//protected $bUseAutoload = true;

/**
 * mothodname TIP: upcHandler Class loader
 * @var string
 */
const upcHandlerClassLoad = "upcClassLoadHandler";

/**
 * @upcHandler
 * @param $sAction string Class to instantize
 * @param $mParams mixed Params to constructor
 * @param $Request UPCRequest Context request
 * @return mixed Object of requested class
 */
protected final function upcClassLoadHandler($sAction, $mParams = NULL, $Request = NULL)
	{
	if(!in_array($sAction,$this->aUPCClassloadList)) return NULL;
	$Result = new $sAction($mParams);
	return $Result;
	}

##
# SHortcuts Handler
##	
	
protected $aUPCShortcutsList = array(); # Варианты
protected $aUPCShortcutsInstructions = array(); # Директивы+кэш
protected $bUPCShortcutsCache = true;

/**
 * mothodname TIP: upcHandler Shortcut handler
 * @var unknown_type
 */
const upcHandlerSortcuts = "upcShortcutCall";

protected final function upcShortcutCall($Step, $mParams = NULL, $Request = NULL)
	{
	# Forbid shortcut handling if it's set in request options
	if(isset($Request) and @$Request->aRequestOptions['ShortcutRequest'])
		return NULL;
		
	# Forced (or cached) shortcut is selected for this step
	if(array_key_exists($Step,$this->aUPCShortcutsInstructions))
		{
		$sSubRequest = $this->aUPCShortcutsInstructions[$Step];
		
		# NULL in cache force to not handle this step 
		if(is_null($sSubRequest))
			return NULL;
			
		# Handle step with according shortcut
		
		$xSubRequest = new UPCRequest($sSubRequest);
		# Set option to block recursion
		$xSubRequest->aRequestOptions['ShortcutRequest'] = true;
		$mResult = $this->call($xSubRequest);
		return $mResult;
		} 
		
	# Trying to handle step through list of shortcuts
	foreach($this->aUPCShortcutsList as $sUPCShortcut)
		{
		$sSubRequest = $this->decodeShortcut($sUPCShortcut,$Step,$mParams);
		$xSubRequest = new UPCRequest($sSubRequest);
		# Set option to block recursion
		$xSubRequest->aRequestOptions['ShortcutRequest'] = true;
		$mResult = $this->call($xSubRequest);
		if(isset($mResult))
		break;
		}
	
	# Cache shortcut
	if($this->bUPCShortcutsCache)
		$this->aUPCShortcutsInstructions[$Step] = isset($mResult) ? $sSubRequest : NULL;
		
	# Result
	return $mResult;
	}

/**
 * Decodes shortuc string with value
 * @param $sShortcut string Shurtcut string
 * @param $sValue Value of step to apply shortcut to
 * @return string Resulting request string
 */
protected function decodeShortcut($sShortcut,$sValue,$mParams)
	{
	return str_replace("?",$sValue,$sShortcut);
	}
	
}

/**
 * URL Procedure Call Request
 * Internal object to present request
 */
class UPCRequest
{

protected $aRequestSteps = array();
protected $aPassedSteps = array();

##
# Options
##

public $aRequestOptions = array();

/**
 * Node to start execution from
 * @var UPC
 */
protected $xEntryNode = NULL;

/**
 * Constructs Request object from plain form
 * @param mixed $mRequest request in plain form
 */
public function __construct($mRequest = NULL)
	{
	# Construct request scheme
	if(is_array($mRequest))
		$this->aRequestSteps = $mRequest;
	elseif(is_string($mRequest))
		$this->aRequestSteps = self::ExplodeRequestString($mRequest);
	$this->NormalizeRequest();
	}
	
/**
 * Execute request
 * @return mixed result
 */
public function call(UPC $xEntryNode = NULL)
	{
	# Set entry node
	$this->xEntryNode = $xEntryNode;
	$mResult = $this->xEntryNode;
	while($mResult instanceof iUPC and $this->Next())
		{
		list($sStep,$mParams) = $this->NextParsed();
		$this->Slice();
		$mResult = $mResult->process($sStep,$mParams,$this);
		}
	return $mResult;
	}
	
/**
 * Parsing string form of request
 * @param string $sRequest
 * @return string[] Request separated by steps
 */
public static function ExplodeRequestString($sRequest)
	{
	//TODO: Handle URLdecode or smth
	
	# Разделителя в строке нет - запрос в один шаг
	if(strpos($sRequest,"/")===false)
		return array($sRequest);
		
	# Запрос в несколько шагов
	$aRequest = array();
	$aRequestChuncks = explode('"',$sRequest);
		foreach($aRequestChuncks as $index=>$sChunk)
			{
			if($index%2) // In quotes
				{
				$sAdd = '"'.$sChunk.'"';
				if(sizeof($aRequest))
					$aRequest[sizeof($aRequest)-1] .= $sAdd;
				else
					$aRequest []= $sAdd;
				}
			else // Out of quotes
				{
				$aChunkSteps = explode("/",$sChunk);
				if(sizeof($aRequest))
					$aRequest[sizeof($aRequest)-1] .= $aChunkSteps[0];
				else
					$aRequest []= $aChunkSteps[0];
				$aChunkSteps = array_slice($aChunkSteps,1);
				$aRequest = array_merge($aRequest,$aChunkSteps);
				}
			}
	return $aRequest;
	}
	
protected function NormalizeRequest()
	{
	$aRequest = $this->aRequestSteps;
	# Remove empty elements from request array
	foreach ($aRequest as $key=>$sNext)
		{
		if(!$sNext)
			unset($aRequest[$key]);	
		}
	$this->aRequestSteps = array_values($aRequest);
	}
	
/**
 * Get next step in plain form
 * @return string Next step
 */
public function Next()
	{
	return reset($this->aRequestSteps);
	}
	
/**
 * Parse plaintext step as Method(Params)
 * @param string $sNext
 * @return array Next step [string $sMethod, mixed $mParams]
 */
public static function Parse($sNext)
	{
	if(!$sNext) return array(NULL,NULL);
	# Already parsed
	if(is_array($sNext)) return $sNext;
	
	# Parsing steps into method + parameters form
	$nMethodLength = strpos($sNext,"(");
	if($nMethodLength===false)
		# No parameters
		{
		$sMethod = $sNext;
		$mParams = null;
		}
	else
		{
		$sMethod = substr($sNext,0,$nMethodLength);
		$sParams = substr($sNext,$nMethodLength+1);
		$mParams = ($sParams) ? self::ParseParams($sParams) : NULL;
		}		
	return array($sMethod, $mParams);
	}
	
/**
 * Parse params list
 * @param $sParams String to parse as params list
 * @return mixed Params
 */
public static function ParseParams($sParams=NULL)
	{
	# No Params
	if(!isset($sParams) or $sParams==="") return NULL;
	# Cut off braces and trim
	if($sParams[0]=="(") $sParams = substr($sParams,1);
	if($sParams[strlen($sParams)-1]==")") $sParams = substr($sParams,0,-1);
	$sParams = trim($sParams);
	# Empty Params string
	if($sParams==="") return NULL;
	# JSON decode
	if(in_array($sParams[0],array('"','{','[')))
		{
		$mParams = json_decode($sParams,true);
		if(!isset($mParams)) 
			{
			trigger_error("Invalid JSON in UPC Params",E_USER_NOTICE);
			$mParams = $sParams;	
			}
		return $mParams;
		}
	# Return as string
	$mParams = $sParams;
	return $mParams;
	}
	
/**
 * Get next step in complex (method+params) form
 * @return array Next step [string $sMethod, mixed $mParams]
 */
public function NextParsed()
	{
	$sNext = $this->Next();
	return self::Parse($sNext);
	}
		
/**
 * Return request in string form
 * for using in modules with their own logic
 * @return string Request
 */
public function RequestString()
	{
	return implode("/",array_merge($this->aPassedSteps,$this->aRequestSteps));
	}
	
/**
 * Return passed steps in string form
 * @return string Passed steps
 */	
public function PassedString()
	{
	return implode("/",$this->aPassedSteps);
	}
	
public function RestString()
	{
	return implode("/",$this->aRequestSteps);
	}
	
/**
 * Slice request array
 * @param $mTargets integer Quantity of steps to slice
 * @return UPCRequest
 */
public function Slice($mTargets = 1)
	{
	$aSliceTargets = array_slice($this->aRequestSteps,0,$mTargets);
	$this->aPassedSteps = array_merge($this->aPassedSteps, $aSliceTargets);
	$this->aRequestSteps = array_slice($this->aRequestSteps,$mTargets);
	return $this;
	}
}